package pl.polsl.mmm.projectcomm.objects.MD5;

import android.opengl.GLES20;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import static android.opengl.GLES20.GL_TRIANGLES;
import static pl.polsl.mmm.projectcomm.Constants.*;

import pl.polsl.mmm.projectcomm.data.vec3;
import pl.polsl.mmm.projectcomm.data.vec4;
import pl.polsl.mmm.projectcomm.objects.Lines;
import pl.polsl.mmm.projectcomm.programs.ColorShaderProgram;
import pl.polsl.mmm.projectcomm.programs.TextureLightShaderProgram;
import pl.polsl.mmm.projectcomm.programs.TextureShaderProgram;

import static android.opengl.GLES20.GL_ARRAY_BUFFER;
import static android.opengl.GLES20.GL_DYNAMIC_DRAW;
import static android.opengl.GLES20.GL_ELEMENT_ARRAY_BUFFER;
import static android.opengl.GLES20.GL_STATIC_DRAW;
import static android.opengl.GLES20.GL_UNSIGNED_SHORT;
import static android.opengl.GLES20.glBindBuffer;
import static android.opengl.GLES20.glBufferData;
import static android.opengl.GLES20.glBufferSubData;
import static android.opengl.GLES20.glDeleteBuffers;
import static android.opengl.GLES20.glDrawElements;
import static android.opengl.GLES20.glEnableVertexAttribArray;
import static android.opengl.GLES20.glGenBuffers;
import static android.opengl.GLES20.glVertexAttribPointer;

/**
 * Created by Maciek on 2014-11-01.
 */
public class MD5Mesh {

    private boolean visible; //widoczność mesha
    private int	mode; //tryb rysowania wierzchołków ( GL_TRIANGLES , GL_TRIANGLES_STRIP itp...)
    private String textureName; //nazwa shadera obsługującego tego mesha

    private int nVertex; //ile obiektów typu vertex w tablicy
    private MD5Vertex md5vertex[]; //tablica obiektów typu vertex

    private int vbo[];//tablica z identyfikatorem bufora w api openGL
    private int vertexDataSize;// wielkość danych w buforze: (ile danych dla wierzchołków) * ilość bajtów
    private FloatBuffer vertexData;//bufor wpisywany dynamicznie do pamięci openGL

    private int nTriangle; //ilość trójkątów
    private short triangle[][]; //indeksy wierzchołków składających się na trójkąty konkretne
    // triangle[numer trójkąta][0/1/2 wierzchołek trójkąta]

    private int	vboIndice[]; //tablica z identyfikatorem bufora w api OpenGL
    private int nIndice; // ilość wierzchołków = nTriangle * 3
    private ShortBuffer indice; //bufor statyczny wpisywany do pamięci OpenGL

    private int	nWeight;//wielkość tablicy z wagami
    private MD5Weight md5weight[];//tablica z wagami jointów

    private int texture;
    //OBJMATERIAL		*objmaterial;

    private Lines normalLines; //obiekt do wyświetlania linii normali ścian na meshu

    public MD5Mesh() {
    }
    /** Przypisanie wskaźników na odpowiednie bufory wierzchołków i rysowanie obiektu */
    public void drawSetAttribPointer (TextureLightShaderProgram textureProgram) {
        //o tym co się tutaj dzieje:
        //http://www.learnopengles.com/android-lesson-seven-an-introduction-to-vertex-buffer-objects-vbos/
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);

        //pozycje wierzchołków
        glVertexAttribPointer(  textureProgram.getPositionAttributeLocation(),
                POSITION_COMPONENT_COUNT,
                GLES20.GL_FLOAT,
                false,
                STRIDE,//STRIDE,
                0);//bez offsetu
        glEnableVertexAttribArray(textureProgram.getPositionAttributeLocation());
        //wektory normalne wierzchołków
        glVertexAttribPointer(  textureProgram.getNormalsAttributeLocation(),
                NORMALS_COMPONENT_COUNT,
                GLES20.GL_FLOAT,
                false,
                STRIDE,
                POSITION_COMPONENT_COUNT * BYTES_PER_FLOAT);
        glEnableVertexAttribArray(textureProgram.getNormalsAttributeLocation());
        //pozycje texeli
        glVertexAttribPointer(  textureProgram.getTextureCoordinatesAttributeLocation(),
                TEXTURE_COORDINATES_COMPONENT_COUNT,
                GLES20.GL_FLOAT,
                false,
                STRIDE,
                (POSITION_COMPONENT_COUNT + NORMALS_COMPONENT_COUNT)
                        * BYTES_PER_FLOAT);
        glEnableVertexAttribArray(textureProgram.getTextureCoordinatesAttributeLocation());
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndice[0]);
        //rysowanie
        glDrawElements(GL_TRIANGLES,nIndice,GL_UNSIGNED_SHORT,0);
        //dobra praktyka: odbindować bufor
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    /** Stworzenie i przypisanie buforów w api OpenGl */
    public void buildVbo () {
        //patrzeć tutaj:
        //http://www.learnopengles.com/android-lesson-seven-an-introduction-to-vertex-buffer-objects-vbos/
        //całkowita wielkość danych w bajtach
        vertexDataSize = STRIDE * nVertex;//stride jest juz pomnożony przez ilość bajtów we floacie
        vertexData = ByteBuffer.allocateDirect(vertexDataSize)
                .order(ByteOrder.nativeOrder()).asFloatBuffer();
        vertexData.position(0);
        vbo = new int[1];
        glGenBuffers(1, vbo, 0);
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
        glBufferData(GL_ARRAY_BUFFER,
                vertexData.capacity() * BYTES_PER_FLOAT,
                vertexData,
                GL_DYNAMIC_DRAW);

        indice.position(0);
        vboIndice = new int[1];
        glGenBuffers(1, vboIndice, 0);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndice[0]);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                indice.capacity() * BYTES_PER_SHORT,
                indice,
                GL_STATIC_DRAW);
        //unbind, podobno istotne! Dobra praktyka!!
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);
        glBindBuffer(GL_ARRAY_BUFFER,0);
        indice.clear();
        indice = null;
    }
    /** Kasowanie buforów w api OpenGl*/
    public void deleteVbo () {
        //kasowanie buforów w api OpenGl'a
        glDeleteBuffers(1,vbo,0);
        glDeleteBuffers(1,vboIndice,0);
    }
    /** Metoda czyszcząca obiekt i odniesienia w api OpenGl*/
    public void cleanUp () {
        deleteVbo();
        triangle = null;
        md5vertex = null;
        md5weight = null;
        vertexData.clear();
        vertexData = null;
        indice.clear();
        indice = null;
    }
    /** Metoda do alokacji tablicy indeksów trójkątów
     * Uwaga: ustawia od razu zmienną nTriangle!!*/
    public void alocateTriangle(int nTriangle) {
        this.nTriangle = nTriangle;
        triangle = new short[nTriangle][3];
    }
    /** Metoda do alokacji tablicy obiektów typu vertex
     * Uwaga: ustawia od razu zmienną nVertex!! */
    public void alocateVertex(int nVertex) {
        this.setnVertex(nVertex);
        md5vertex = new MD5Vertex[nVertex];
    }
    /** Metoda do alokacji tablicy z wagami
     * Uwaga: ustawia od razu zmienną nWeight!!*/
    public void alocateWeight(int nWeight) {
        this.nWeight = nWeight;
        md5weight = new MD5Weight[nWeight];
    }
    /** Metoda do alokacji bufora indeksów.
     *  Bufor wykorzystywany do tworzenia odpowiedniego bufora w api OpenGl
     *  Uwaga: nie wymaga parametru nIndice, korzysta z już ustawionego wcześniej pola!!*/
    public void alocateIndice() {
        indice = ByteBuffer.allocateDirect(nIndice * BYTES_PER_SHORT)
                .order(ByteOrder.nativeOrder())
                .asShortBuffer();
    }
    /** Metoda do stworzenia tablicy linii normali (do debugu) */
    public void alocateNormalLines() {
        normalLines = new Lines(nVertex);
    }

    /** Metoda do tworzenia/wklejania do tablicy nowych obiektów typu MD5Vertex*/
    public void insertVertex(MD5Vertex newVertex, int index) {
        md5vertex[index] = new MD5Vertex(newVertex);
    }
    /** Metoda do tworzenia/wklejania do tablicy 3 indeksów wierzchołków tworzących trójkąt*/
    public void insertTriangle(short x, short y, short z, int index) {
        triangle[index][2] = x;
        triangle[index][1] = y;
        triangle[index][0] = z;
    }
    /** Metoda do wklejania wszystkich danych potrzebnych do renderowania do bufora vertex data*/
    public void insertVertexData() {
        //zerowanie pozycji
        vertexData.position(0);

        for (int i = 0; i < nVertex; i++) {
            //wrzucenie do bufora wektora pozycji
            vertexData.put(md5vertex[i].getPosition().x);
            vertexData.put(md5vertex[i].getPosition().y);
            vertexData.put(md5vertex[i].getPosition().z);
            //wrzucenie do bufora wektora normalnego
            vertexData.put(md5vertex[i].getNormal().x);
            vertexData.put(md5vertex[i].getNormal().y);
            vertexData.put(md5vertex[i].getNormal().z);
            //wrzucenie do bufora wektora UV
            vertexData.put(md5vertex[i].getUv().u);
            vertexData.put(md5vertex[i].getUv().v);
        }
        //ponowne zerowanie pozycji
        vertexData.position(0);
        //zbindowanie bufora
        glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
        //wrzucenie danych
        glBufferSubData(GL_ARRAY_BUFFER,
                0, //offset
                vertexData.capacity() * BYTES_PER_FLOAT, //ilość danych
                vertexData); //bufor zawierający dane do skopiowania
        //odbindowanie bufora
        glBindBuffer( GL_ARRAY_BUFFER, 0 );
    }
    /** Metoda do tworzenia/wklejania do tablicy nowych obiektów typu MD5Weight */
    public void insertWeight(MD5Weight weight, int index) {
        md5weight[index] = new MD5Weight(weight);
    }
    /** Metoda do umieszczania w buforze indeksów kolejnych trójkątów */
    public void insertIndices() {
        indice.position(0);
        for(int i=0;i<nTriangle;i++) {
            indice.put(triangle[i][0]);
            indice.put(triangle[i][1]);
            indice.put(triangle[i][2]);
        }
    }
    /** Wyliczenie pozycji vertexów mesha na podstawie kości i tablicy wag */
    public void prepareMesh(MD5Joint[] pose) {
        vec3 locationTemp = new vec3();
        for(int i=0;i<nVertex;i++) {
            //inicjalizacja wektorów: pozycji i normalnego
            md5vertex[i].resetVectors();
            for(int j=0;j<md5vertex[i].getWeightCount();j++) {
                //skaczemy po kolejnych wagach wierzchołków
                MD5Weight weight = md5weight[md5vertex[i].getStartWeight() + j];
                //i odpowiadających im kościach
                MD5Joint joint = pose[weight.getJoint()];
                //pobranie lokacji z tabeli wag i przemnożenie przez wektor obrotu kości
                locationTemp.rotateVec4(weight.getLocation(),joint.getRotation());
                //mnożemy przez bias wszystkie składowe wektora i sumujemy
                md5vertex[i].getPosition().add(
                        (joint.getLocation().x + locationTemp.x) * weight.getBias(),
                        (joint.getLocation().y + locationTemp.y) * weight.getBias(),
                        (joint.getLocation().z + locationTemp.z) * weight.getBias());
            }
        }
    }
    /** Wyliczenie wektorów normalnych mesha poprzez operację crossProduct na wszystkich 3 wierzchołkach
     * składających się na trójkąt*/
    public void prepareNormals(MD5Joint[] pose) {
        vec3 v1 = new vec3(),
                v2 = new vec3(),
                normal = new vec3();
        //operacja crossProduct do wyliczenia wektora normalnego.
        for(int j=0;j<nTriangle;j++) {

            //pobieramy aktualnie badany trójkąt
            short currTriangle[] = triangle[j];
            v1 .diff(
                    md5vertex[currTriangle[0]].getPosition(),
                    md5vertex[currTriangle[2]].getPosition());
            v2.diff(
                    md5vertex[currTriangle[1]].getPosition(),
                    md5vertex[currTriangle[2]].getPosition());
            normal.crossProduct(v1,v2);
            //normal.normalize();
            //ustawienie wektorów normalnych
            md5vertex[currTriangle[0]].getNormal().add(normal);
            md5vertex[currTriangle[1]].getNormal().add(normal);
            md5vertex[currTriangle[2]].getNormal().add(normal);
        }
        //normalizacja otrzymanych wektorów
        for(int i=0;i<nVertex;i++) {
            md5vertex[i].getNormal().normalize();
            //wektor gotowy do wrzucenia do bufora normali
        }
        /*
        //UWAGA
        //teraz dzieje się pewien trick
        for(int i=0;i<nWeight;i++) {
            //obiekt weight w zasadzie nie potrzebuje pola vec3 Normal
            //czemu więc tutaj jest?
            md5weight[i].resetNormal();
        }
        //po to byśmy mogli wyliczyć sobie normale w przestrzeni lokalnej kości
        //ażeby później animowane normale były liczone dużo szybciej
        for(int j=0;j<nVertex;j++) {
            MD5Vertex vertex = md5vertex[j];
            for(int k=0;k<vertex.getWeightCount();k++) {
                normal = new vec3(vertex.getNormal());
                MD5Weight weight = md5weight[vertex.getStartWeight() + k];
                MD5Joint joint = pose[weight.getJoint()];
                vec4 rotation = joint.getRotation();
                //musimy obrócić przez odwrotność!!
                rotation.conjugate(rotation);
                normal.rotateVec4(normal,rotation);
                weight.getNormal().add(normal);
            }
        }
        //też trzeba znormalizować jak poprzednio!
        for(int i=0;i<nWeight;i++) {
            md5weight[i].getNormal().normalize();
        }
        */
    }

    /** Wklejenie do tablicy wierzchołków linii*/
    public void drawDebugNormals(ColorShaderProgram colorProgram) {
        for(int i = 0;i<nVertex;i++){
            normalLines.putNormalData(
                    md5vertex[i].getPosition(),
                    md5vertex[i].getNormal());
        }
        normalLines.bindData(colorProgram);
        normalLines.drawLines();
    }

    /******************* SETTERY I GETTERY********************************/
    public int getMode() {
        return mode;
    }

    public void setMode(int mode) {
        this.mode = mode;
    }

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public String getTextureName() {
        return textureName;
    }

    public void setTextureName(String textureName) {
        this.textureName = textureName;
    }

    public int getnTriangle() {
        return nTriangle;
    }

    public void setnTriangle(int nTriangle) {
        this.nTriangle = nTriangle;
    }

    public int getnIndice() {
        return nIndice;
    }

    public void setnIndice(int nIndice) {
        this.nIndice = nIndice;
    }

    public int getnWeight() {
        return nWeight;
    }

    public void setnWeight(int nWeight) {
        this.nWeight = nWeight;
    }

    public int getVertexDataSize() {
        return vertexDataSize;
    }

    public void setVertexDataSize(int vertexDataSize) {
        this.vertexDataSize = vertexDataSize;
    }

    public int getnVertex() {
        return nVertex;
    }

    public void setnVertex(int nVertex) {
        this.nVertex = nVertex;
    }

    public ShortBuffer getIndice() {
        return indice;
    }

    public int getTexture() {
        return texture;
    }

    public void setTexture(int texture) {
        this.texture = texture;
    }

    public Lines getNormalLines() {
        return normalLines;
    }
}
